/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.datatheorem.ohos.trustkit.pinning;

import com.datatheorem.ohos.trustkit.config.DomainPinningPolicy;
import com.datatheorem.ohos.trustkit.config.PublicKeyPin;

import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Set;
import javax.net.ssl.X509TrustManager;



class PinningTrustManager implements X509TrustManager {

    // The trust manager we use to do the default SSL validation
    //TODO 暂无该方法
//    private final X509TrustManagerExtensions baselineTrustManager;

    private final String serverHostname;
    private final DomainPinningPolicy serverConfig;


    /**
     * A trust manager which implements path, hostname and pinning validation for a given hostname
     * and sends pinning failure reports if validation failed.
     *
     * @param serverHostname: The hostname of the server whose identity is being validated. It will
     *                      be validated against the name(s) the leaf certificate was issued for
     *                      when performing hostname validation.
     * @param serverConfig: The pinning policy to be enforced when doing pinning validation.
     * @param baselineTrustManager: The trust manager to use for path validation.
     */
    public PinningTrustManager( String serverHostname,
                                DomainPinningPolicy serverConfig,
                                X509TrustManager baselineTrustManager) {
        // Store server's information
        this.serverHostname = serverHostname;
        this.serverConfig = serverConfig;

        //TODO
//        this.baselineTrustManager = new X509TrustManagerExtensions(baselineTrustManager);

    }

    /**
     *
     * For now this is here only for documentation.
     * not to be confused with X509TrustManagerExtensions!
     *
     * @param chain X509Certificate[]
     * @param authType String
     * @throws CertificateException
     */
    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
        boolean didChainValidationFail = false; // Includes path and hostname validation
        boolean didPinningValidationFail = false;

        // Store the received chain so we can send it later in a report if path validation fails
        List<X509Certificate> servedServerChain = Arrays.asList((X509Certificate [])chain);
        List<X509Certificate> validatedServerChain = servedServerChain;

        // Then do hostname validation first
        // During the normal flow, this is done at very different times during the SSL handshake,
        // depending on the device's API level; we just do it here to ensure it is always done
        // consistently
        if (!OkHostnameVerifier.INSTANCE.verify(serverHostname, chain[0])) {
            didChainValidationFail = true;
        }

        if ((!didChainValidationFail)) {
            boolean hasPinningPolicyExpired = (serverConfig.expirationDate != null)
                    && (serverConfig.expirationDate.compareTo(new Date()) < 0);
            // Only do pinning validation if the policy has not expired
            if (!hasPinningPolicyExpired) {
                didPinningValidationFail = !isPinInChain(validatedServerChain,
                        serverConfig.getPublicKeyPins());
            }
        }

        // Send a pinning failure report if needed
        if (didChainValidationFail || didPinningValidationFail) {
            PinningValidationResult validationResult = PinningValidationResult.FAILED;
            if (didChainValidationFail) {
                // Hostname or path validation failed - not a pinning error
                validationResult = PinningValidationResult.FAILED_CERTIFICATE_CHAIN_NOT_TRUSTED;
            }
            TrustManagerBuilder.getReporter().pinValidationFailed(serverHostname, 0,
                    servedServerChain, validatedServerChain, serverConfig, validationResult);
        }

        // Throw an exception if needed
        if (didChainValidationFail) {
            throw new CertificateException("Certificate validation failed for " + serverHostname);
        } else if ((didPinningValidationFail) && (serverConfig.shouldEnforcePinning())) {
            // Pinning failed and is enforced - throw an exception to cancel the handshake
            StringBuilder errorBuilder = new StringBuilder()
                    .append("Pin verification failed")
                    .append("\n  Configured pins: ");
            for (PublicKeyPin pin : serverConfig.getPublicKeyPins()) {
                errorBuilder.append(pin);
                errorBuilder.append(" ");
            }
            errorBuilder.append("\n  Peer certificate chain: ");
            for (Certificate certificate : validatedServerChain) {
                errorBuilder.append("\n    ")
                        .append(new PublicKeyPin(certificate))
                        .append(" - ")
                        .append(((X509Certificate) certificate).getSubjectDN());
            }
            throw new CertificateException(errorBuilder.toString());
        }
    }

    private static boolean isPinInChain(List<X509Certificate> verifiedServerChain,
                                        Set<PublicKeyPin> configuredPins) {
        boolean wasPinFound = false;
        for (Certificate certificate : verifiedServerChain) {
            PublicKeyPin certificatePin = new PublicKeyPin(certificate);
            if (configuredPins.contains(certificatePin)) {
                // Pinning validation succeeded
                wasPinFound = true;
                break;
            }
        }
        return wasPinFound;
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
        throw new CertificateException("Client certificates not supported!");
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        // getAcceptedIssuers is meant to be used to determine which trust anchors the server will
        // accept when verifying clients.
        return new X509Certificate[0];
    }
}
